Fluge Site

最近在看网络编程模型,虽然Golang天然高并发的原因很大一部分是因为协程channel,但是这个里面还是离不开底层的网络编程模型的选用 — epoll。在学习这部分的时候对IO多路复用做了一些了解。

一些概念

内核空间和用户空间

现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。

为了保证用户进程不能直接操作内核,保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。我们可以简单理解为,一张纸,四分之一给一个叫内核的人用,四分之三给一个叫用户的人用。

fd 文件操作描述符

文件描述符在形式上是一个非负整数,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。文件描述符这一概念往往只适用于Unix和Linux这样的操作系统。

缓存IO

缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。就是从内核空间用户空间需要经过两次复制操作。这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

Linux的socket 事件wakeup callback机制

Linux(2.6+)内核 会通过sleep_list等待队列,去管理所有正在等socket事件的process,会在sleep_list 中为当前正在等待的process构建一个wait_entry,只到超时或者事件触发,在每个wait_enrey上会定义一个callback
同时wakeup机制会异步的唤醒整个sleep_list上的process,并同时执行callback ,删除sleep_list 上的wait_entry 节点。总体上会涉及两大逻辑:

睡眠机制

  1. selectpollepoll_wait陷入内核,判断监控的socket是否有关心的事件发生了,如果没,则为当前process构建一个wait_entry节点,然后插入到监控socket的sleep_list里取。
  2. Linux调用schedule函数进行process的状态转换,shcedule函数是Linux的调度process的函数,这里指的是process进入sleep直到超时或者事件发生。
  3. 事件触发后,将当前process的wait_entry节点从socket的sleep_list中删除。

唤醒机制

  1. socket的事件发生了,然后socket顺序遍历其睡眠队列sleep_list,依次调用每个wait_entry节点(对应各个Process)的callback函数。
  2. 直到完成队列的遍历或遇到某个wait_entry节点是排他的才停止。
  3. 一般情况下callback包含两个逻辑:wait_entry自定义的私有逻辑和唤醒的公共逻辑,主要用于将该wait_entry的process放入CPU的就绪队列,让CPU随后可以调度其执行。

IO模型

对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

  1. 等待数据准备 (Waiting for the data to be ready)
  2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

正式因为这两个阶段,linux系统产生了下面五种网络模式的方案:

  1. 阻塞 I/O(blocking IO)
  2. 非阻塞 I/O(nonblocking IO)
  3. I/O 多路复用( IO multiplexing)
  4. 信号驱动 I/O( signal driven IO)(比较少用到)
  5. 异步 I/O(asynchronous IO)
阻塞 I/O(blocking IO)


用户进程process在Blocking IO读recvfrom操作的两个阶段都是等待的。在数据没准备好的时候,process原地等待kernel准备数据。kernel准备好数据后,process继续等待kernel将数据copy到自己的buffer。在kernel完成数据的copy后process才会从recvfrom系统调用中返回。

非阻塞 I/O(NonBlocking IO)


process在NonBlocking IOrecvfrom操作的第一个阶段是不会block等待的,如果kernel数据还没准备好,那么recvfrom会立刻返回一个EWOULDBLOCK错误。当kernel准备好数据后,进入处理的第二阶段的时候,process会等待kernel将数据copy到自己的buffer,在kernel完成数据的copy后process才会从recvfrom系统调用中返回。

IO多路复用(NonBlocking IO)


IO多路复用,就是我们熟知的selectpollepoll模型。从图上可见,在IO多路复用的时候,process在两个处理阶段都是block住等待的。初看好像IO多路复用没什么用,其实select、poll、epoll的优势在于·可以以较少的代价来同时监听处理多个IO